Over-the-Air NVIDIA Jetson - RAUC Update Flow and Custom Bootloader

From RidgeRun Developer Wiki

Follow Us On Twitter LinkedIn Email Share this page




NVIDIA partner logo NXP partner logo





RAUC on NVIDIA Jetson: Update Flow and Custom Bootloader Backend

This page documents the complete signed update flow after the base RAUC integration is already working.

It covers:

  • Generating signing keys
  • Installing the correct CA into the image
  • Creating a RAUC bundle recipe
  • Installing signed bundles on target
  • Manual A/B switching via extlinux
  • Replacing noop with a custom bootloader backend

Assumptions

This page assumes:

  • RAUC is already integrated in the image
  • rauc.service starts successfully
  • Slot detection already works on target
  • root= and rauc.slot= are already present in the kernel cmdline

RAUC Bundle Creation and A/B Update Validation

1. Generating Signing Keys

RAUC requires:

  • A CA certificate installed on target
  • A signing key + certificate used to sign bundles

The helper script from meta-rauc was used:

bash layers/meta-rauc/scripts/openssl-ca.sh \
    layers/meta-tegrademo/recipes-core/rauc/rauc-keys/files

This generated:

  • openssl-ca/dev/ca.cert.pem
  • openssl-ca/dev/private/development-1.key.pem
  • openssl-ca/dev/development-1.cert.pem

Verification:

openssl x509 -in openssl-ca/dev/ca.cert.pem -noout -fingerprint -sha256

2. Installing the Correct CA into the Image

Initially, the target contained the default RAUC test CA:

openssl x509 -in /etc/rauc/ca.cert.pem -noout -subject

Output:

CN=RAUC Test CA

This caused bundle verification failure.

Error Observed

signature verification failed:
unable to get local issuer certificate

Root Cause

The bundle was signed with the newly generated CA, but the target still trusted the default RAUC Test CA.

Fix

Copy the generated CA into the BSP layer:

cp openssl-ca/dev/ca.cert.pem \
   layers/meta-tegrademo/recipes-core/rauc/rauc-conf/files/ca.cert.pem

Then rebuild and reflash the image.

After reflashing:

openssl x509 -in /etc/rauc/ca.cert.pem -noout -fingerprint -sha256

Fingerprint matched the build CA.

3. Creating the RAUC Bundle Recipe

A custom bundle recipe was created:

meta-tegrademo/recipes-core/rauc/demo-bundle/demo-bundle.bb

Minimal example:

SUMMARY = "Demo RAUC bundle for demo-image-base"
LICENSE = "MIT"

inherit bundle

RAUC_BUNDLE_COMPATIBLE = "jetson-agx-orin"
RAUC_BUNDLE_FORMAT = "plain"

RAUC_BUNDLE_SLOTS = "rootfs"

RAUC_SLOT_rootfs = "demo-image-base"
RAUC_SLOT_rootfs[fstype] = "ext4"
RAUC_SLOT_rootfs[file] = "demo-image-base-${MACHINE}.rootfs.ext4"

RAUC_KEY_FILE  = "${TOPDIR}/../openssl-ca/dev/private/development-1.key.pem"
RAUC_CERT_FILE = "${TOPDIR}/../openssl-ca/dev/development-1.cert.pem"

Build:

bitbake demo-bundle

Result:

tmp/deploy/images/<machine>/demo-bundle-<machine>.raucb

4. Installing the Bundle on Target

Copy bundle:

scp demo-bundle-*.raucb root@target:/root/

Install:

rauc install /root/demo-bundle-*.raucb

Successful output:

100% Installing done.
Installing succeeded

The image was written to:

/dev/mmcblk0p2 (rootfs.1)

5. Slot State After Installation

Immediately after installation:

rauc status

Output:

  • Booted from rootfs.0 (A)
  • rootfs.1 marked inactive
  • bootloader shown as none (noop backend)

Important:

With bootloader=noop, RAUC does NOT switch slots automatically.

6. Manual Slot Switching (extlinux)

Jetson uses extlinux.

Edit:

/boot/extlinux/extlinux.conf

Original:

APPEND ... root=/dev/mmcblk0p1 rauc.slot=A

Modified:

APPEND ... root=/dev/mmcblk0p2 rauc.slot=B

Reboot the device.

Verification:

findmnt -n -o SOURCE /
cat /proc/cmdline | grep rauc.slot

System should now boot from:

/dev/mmcblk0p2
rauc.slot=B

7. Marking the Slot Good

After confirming successful boot:

rauc status mark-good booted

This marks the currently booted slot as valid.

Note:

With noop backend, this does not affect bootloader state, but RAUC internal state is updated.

Implementing a Custom Bootloader Backend for Jetson (extlinux-based)

Motivation

Because bootloader=noop does not allow persistent slot activation, a custom backend was introduced to make slot switching visible to RAUC.

Step 1: Splitting extlinux into A/B Entries

The first requirement was to define independent boot entries for each rootfs.

File modified:

/boot/extlinux/extlinux.conf

Final structure:

# L4TLauncher configuration file generated by OE4T
MENU TITLE L4T boot options
DEFAULT rauc-B
TIMEOUT 30

LABEL rauc-A
    MENU LABEL RAUC slot A
    LINUX /boot/Image
    INITRD /boot/initrd
    APPEND ${cbootargs} root=/dev/mmcblk0p1 rauc.slot=A mminit_loglevel=4 console=tty0 console

LABEL rauc-B
    MENU LABEL RAUC slot B
    LINUX /boot/Image
    INITRD /boot/initrd
    APPEND ${cbootargs} root=/dev/mmcblk0p2 rauc.slot=B mminit_loglevel=4 console=tty0 console

Key design decisions:

  • Each slot has its own LABEL.
  • Each slot explicitly defines:
    • root=
    • rauc.slot=
  • DEFAULT determines which slot will boot next.

Step 2: Enabling Custom Bootloader in RAUC

The RAUC configuration was modified to use a custom backend.

File:

/etc/rauc/system.conf

Final configuration:

[system]
bootloader=custom
compatible=jetson-agx-orin
statusfile=/var/lib/rauc/status

[keyring]
path=/etc/rauc/ca.cert.pem

[handlers]
bootloader-custom-backend=/usr/lib/rauc/backend/jetson-extlinux

[slot.rootfs.0]
device=/dev/mmcblk0p1
type=ext4
bootname=A

[slot.rootfs.1]
device=/dev/mmcblk0p2
type=ext4
bootname=B

Important:

  • bootloader=custom activates the custom backend interface.
  • RAUC will call the backend binary for:
    • get-primary
    • set-primary
    • get-booted
    • get-state
    • set-state

Step 3: Implementing the Custom Backend Script

The backend was implemented as a simple POSIX shell script:

/usr/lib/rauc/backend/jetson-extlinux

Implementation:

#!/bin/sh
set -eu

STATE_DIR="/var/lib/rauc"
PRIMARY_FILE="${STATE_DIR}/primary"
STATE_A="${STATE_DIR}/bootstate.A"
STATE_B="${STATE_DIR}/bootstate.B"
EXTLINUX_CONF="${EXTLINUX_CONF:-/boot/extlinux/extlinux.conf}"

mkdir -p "${STATE_DIR}"

usage() {
  echo "Supported commands: get-primary set-primary <A|B> get-booted get-state <A|B> set-state <A|B> <good|bad>" >&2
  exit 1
}

norm_slot() {
  case "${1:-}" in
    A|B) echo "$1" ;;
    *) usage ;;
  esac
}

norm_state() {
  case "${1:-}" in
    good|bad) echo "$1" ;;
    *) usage ;;
  esac
}

get_state_file() {
  case "$1" in
    A) echo "$STATE_A" ;;
    B) echo "$STATE_B" ;;
  esac
}

slot_to_label() {
  case "$1" in
    A) echo "rauc-a" ;;
    B) echo "rauc-b" ;;
  esac
}

label_to_slot() {
  case "${1:-}" in
    rauc-a|rauc-A|A) echo "A" ;;
    rauc-b|rauc-B|B) echo "B" ;;
    *) return 1 ;;
  esac
}

get_primary_from_extlinux() {
  [ -f "${EXTLINUX_CONF}" ] || return 1
  label="$(awk '/^[[:space:]]*DEFAULT[[:space:]]+/ {print $2; exit}' "${EXTLINUX_CONF}")"
  label_to_slot "${label}"
}

set_primary_in_extlinux() {
  slot="$1"
  [ -f "${EXTLINUX_CONF}" ] || return 0

  label="$(slot_to_label "${slot}")"
  tmp="$(mktemp)"
  awk -v new_default="${label}" '
    BEGIN { changed = 0 }
    /^[[:space:]]*DEFAULT[[:space:]]+/ {
      print "DEFAULT " new_default
      changed = 1
      next
    }
    { print }
    END {
      if (!changed)
        print "DEFAULT " new_default
    }
  ' "${EXTLINUX_CONF}" > "${tmp}"
  cat "${tmp}" > "${EXTLINUX_CONF}"
  rm -f "${tmp}"
}

cmd="${1:-}"
shift || true

case "$cmd" in
  get-primary)
    if get_primary_from_extlinux >/dev/null 2>&1; then
      get_primary_from_extlinux
      exit 0
    fi
    if [ -f "${PRIMARY_FILE}" ]; then
      cat "${PRIMARY_FILE}"
      exit 0
    fi
    echo "A"
    exit 0
    ;;

  set-primary)
    slot="$(norm_slot "${1:-}")"
    set_primary_in_extlinux "${slot}"
    echo "${slot}" > "${PRIMARY_FILE}"
    exit 0
    ;;

  get-booted)
    if grep -q 'rauc.slot=' /proc/cmdline 2>/dev/null; then
      sed -n 's/.*rauc\.slot=\([AB]\).*/\1/p' /proc/cmdline | head -n1
      exit 0
    fi
    get_primary_from_extlinux || echo "A"
    exit 0
    ;;

  get-state)
    slot="$(norm_slot "${1:-}")"
    f="$(get_state_file "${slot}")"
    if [ -f "${f}" ]; then
      cat "${f}"
    else
      echo "bad"
    fi
    exit 0
    ;;

  set-state)
    slot="$(norm_slot "${1:-}")"
    st="$(norm_state "${2:-}")"
    f="$(get_state_file "${slot}")"
    echo "${st}" > "${f}"
    exit 0
    ;;

  *)
    usage
    ;;
esac

How Slot Switching Works with This Backend

The switching process is now:

  1. rauc install bundle.raucb writes the new image to the inactive slot
  2. RAUC calls set-primary <slot>
  3. The backend updates the primary slot and extlinux DEFAULT
  4. On next reboot:
    1. extlinux selects the target LABEL
    2. kernel cmdline exposes rauc.slot
    3. RAUC validates the booted slot and slot state

Manual control is also possible:

/usr/lib/rauc/backend/jetson-extlinux get-primary
/usr/lib/rauc/backend/jetson-extlinux set-primary A
/usr/lib/rauc/backend/jetson-extlinux set-primary B

Validated Result

At the end of this page, the update flow validated was:

  1. Build image with RAUC
  2. Flash image
  3. Start RAUC service
  4. Generate signing keys
  5. Build signed bundle
  6. Install bundle
  7. Write inactive slot
  8. Switch slot
  9. Reboot into updated rootfs

This confirms a working signed A/B update flow using RAUC on Jetson AGX Orin.